#!/bin/dash
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# NOTE: This script works in dash, but is not as featureful.  Specifically,
# dash omits readline support (history & command line editing).  So we try
# to run through bash if it exists, otherwise we stick to dash.  All other
# code should be coded to the POSIX standard and avoid bashisms.
#
# Please test that any changes continue to work in dash by running
# '/build/$BOARD/bin/dash crosh --dash' before checking them in.

# Disable path expansion at the command line.  None of our builtins need or want
# it, and it's safer/saner to disallow it in the first place.
set -f

# Don't allow SIGHUP to terminate crosh. This guarantees that even if the user
# closes the crosh window, we make our way back up to the main loop, which gives
# cleanup code in command handlers a chance to run.
trap '' HUP

# Do not let CTRL+C kill crosh itself.  This does let the user kill commands
# that are run by crosh (like `ping`).
trap : INT

# If it exists, use $DATA_DIR to define the $HOME location, as a first step
# to entirely removing the use of $HOME. (Though HOME should be used as
# fall-back when DATA_DIR is unset.)
# TODO(keescook): remove $HOME entirely crbug.com/333031
if [ "${DATA_DIR+set}" = "set" ]; then
  export HOME="${DATA_DIR}/user"
fi

IS_BASH=0
try_bash() {
  # If dash was explicitly requested, then nothing to do.
  case " $* " in
  *" --dash "*) return 0;;
  esac

  # If we're already bash, then nothing to do.
  if type "history" 2>/dev/null | grep -q "shell builtin"; then
    IS_BASH=1
    return 0
  fi

  # Still here?  Relaunch in bash.
  exec /bin/bash $0 "$@"
}
try_bash "$@"

INTRO_TEXT="Welcome to crosh, the Chrome OS developer shell.

If you got here by mistake, don't panic!  Just close this tab and carry on.

Type 'help' for a list of commands.
"

CHROMEOS_INSTALL=/usr/sbin/chromeos-install

check_digits() {
  expr "$1" : '^[[:digit:]]*$' > /dev/null
}

load_extra_crosh() {
  # Keep `local` decl split from assignment so return code is checked.
  local crosh_dir devmode removable src

  crosh_dir=$(dirname "$0")

  if [ -e /usr/share/misc/chromeos-common.sh ]; then
    . "/usr/share/misc/chromeos-common.sh" || exit 1

    src=$(get_block_dev_from_partition_dev $(rootdev -s))
    if [ "$(cat /sys/block/${src#/dev/}/removable)" = "1" ]; then
      removable=1
    fi
  fi

  case "${removable}: $* " in
  1:*|\
  *:*" --usb "*)
    . "${crosh_dir}/crosh-usb"
    ;;
  esac

  # Force dev behavior on dev images.
  if type crossystem >/dev/null 2>&1; then
    crossystem "cros_debug?1"
    devmode=$((!$?))
  else
    echo "Could not locate 'crossystem'; assuming devmode is off."
  fi

  case "${devmode}: $* " in
  1:*|\
  *:*" --dev "*)
    . "${crosh_dir}/crosh-dev"
    ;;
  esac
}

shell_read() {
  local prompt="$1"
  shift

  if [ "$IS_BASH" -eq "1" ]; then
    # In bash, -e gives readline support.
    read -p "$prompt" -e $@
  else
    read -p "$prompt" $@
  fi
}

shell_history() {
  if [ "$IS_BASH" -eq "1" ]; then
    # In bash, the history builtin can be used to manage readline history
    history "$@"
  fi
}

shell_history_init() {
  # Do not set any HISTXXX vars until after security check below.
  local histfile="${HOME}/.crosh_history"

  # Limit the history to the last 100 entries to keep the file from growing
  # out of control (unlikely, but let's be safe).
  local histsize="100"

  # For security sake, let's clean up the file before we let the shell get a
  # chance to read it.  Clean out any non-printable ASCII chars (sorry UTF8
  # users) as it's the easiest way to be sure.  We limit to 4k to be sane.
  # We need a tempfile on the same device to avoid races w/multiple crosh
  # tabs opening at the same time.
  local tmpfile
  if ! tmpfile="$(mktemp "${histfile}.XXXXXX")"; then
    echo "warning: could not clean up history; ignoring it"
    return
  fi
  # Ignore cat errors in case it doesn't exist yet.
  cat "${histfile}" 2>/dev/null |
    tr -dc '[:print:]\t\n' |
    tail -n "${histsize}" |
    tail -c 4096 > "${tmpfile}"
  if ! mv "${tmpfile}" "${histfile}"; then
    echo "warning: could not clean up history; ignoring it"
    rm -f "${tmpfile}"
    return
  fi

  # Set this before any other history settings as some of them implicitly
  # operate on this file.
  HISTFILE="${histfile}"

  # Now we can limit the size of the history file.
  HISTSIZE=${histsize}
  HISTFILESIZE=${histsize}

  # Do not add entries that begin with a space, and dedupe sequential commands.
  HISTCONTROL="ignoreboth"

  # Initialize pseudo completion support.  Do it before we load the user's
  # history so that new entries don't come after old ones.
  if [ ${IS_BASH} -eq 1 ]; then
    local f
    for f in $(declare -F | sed -n '/-f cmd_/s:.*cmd_::p'); do
      # Do not add duplicates to avoid ballooning history.
      grep -qs "^${f} *$" "${HISTFILE}" || shell_history -s "${f}"
    done
  fi

  # Now load the user's history.
  shell_history -r "${HISTFILE}"
}

# Prints the value corresponding to the passed-in field in the output from
# dump_power_status.
get_power_status_field() {
  local field="$1"
  dump_power_status | awk -v field="${field}" '$1 == field { print $2 }'
}

USAGE_help='[command]'
HELP_help='
  Display general help, or details for a specific command.
'
cmd_help() (
  local cmd

  case $# in
  0)
    local cmds="exit help help_advanced ping top"
    for cmd in ${cmds}; do
      cmd_help "${cmd}"
    done
    ;;
  1)
    cmd="$1"
    if check_command "${cmd}"; then
      local usage="USAGE_${cmd}" help="HELP_${cmd}"
      printf "${cmd} "
      eval printf %s "\"\$${usage}\""
      eval echo "\"  \$${help}\""
    else
      echo "help: unknown command '${cmd}'"
      return 1
    fi
    ;;
  *)
    help "too many arguments"
    return 1
    ;;
  esac
)

# Useful alias for commands to call us.
help() {
  if [ $# -ne 0 ]; then
    printf 'ERROR: %s\n\n' "$*"
  fi

  # This is set by `dispatch`.
  cmd_help "${CURRENT_COMMAND}"
}

USAGE_help_advanced=''
HELP_help_advanced='
  Display the help for more advanced commands, mainly used for debugging.
'
cmd_help_advanced() (
  local cmd commands

  if [ $# -ne 0 ]; then
    help "too many arguments"
    return 1
  fi

  commands=$(set | grep -o '^HELP_[^=]*' | \
             sed -e 's:^HELP_::' -e 's:=$::' | sort)
  for cmd in ${commands}; do
    cmd_help "${cmd}"
  done
)

# We move the trailing brace to the next line so that we avoid the style
# checker from rejecting the use of braces.  We cannot use subshells here
# as we want the set the exit variable in crosh itself.
# http://crbug.com/318368
USAGE_exit=''
HELP_exit='
  Exit crosh.
'
cmd_exit()
{
  exit='y'
}

USAGE_set_time='[<time string>]'
HELP_set_time='
  Sets the system time if the the system has been unable to get it from the
  network.  The <time string> uses the format of the GNU coreutils date command.
'
cmd_set_time() (
  local spec="$*"
  if [ -z "${spec}" ]; then
    echo "A date/time specification is required."
    echo "E.g., set_time 10 February 2012 11:21am"
    echo "(Remember to set your timezone in Settings first.)"
    return
  fi
  local sec status
  sec=$(date +%s --date="${spec}" 2>&1)
  status=$?
  if [ ${status} -ne 0 -o -z "${sec}" ]; then
    echo "Unable to understand the specified time:"
    echo "${sec}"
    return
  fi
  local reply
  reply=$(dbus-send --system --type=method_call --print-reply \
    --dest=org.torproject.tlsdate /org/torproject/tlsdate \
    org.torproject.tlsdate.SetTime "int64:$((sec))" 2>/dev/null)
  status=$?
  if [ ${status} -ne 0 ]; then
    echo "Time not set. Unable to communicate with the time service."
    return
  fi
  # Reply format: <dbus response header>\n    uint32 <code>\n
  local code
  code=$(echo "${reply}" | sed -n -e '$s/.*uint32 \([0-9]\).*/\1/p')
  case "${code}" in
  0)
    echo "Time has been set."
    ;;
  1)
    echo "Requested time was invalid (too large or too small): ${sec}"
    ;;
  2)
    echo "Time not set. Network time cannot be overriden."
    ;;
  3)
    echo "Time not set. There was a communication error."
    ;;
  *)
    echo "An unexpected response was received: ${code}"
    echo "Details: ${reply}"
  esac
)

# Set the vars to pass the unittests ...
USAGE_ssh=''
HELP_ssh=''
# ... then unset them to hide the command from "help" output.
unset USAGE_ssh HELP_ssh
cmd_ssh() (
  cat <<EOF
The 'ssh' command has been removed.  Please install the official SSH extension:
https://chrome.google.com/webstore/detail/pnhechapfaindjhompbnflcldabbghjo
EOF
)

USAGE_swap='[enable <size (MB)> | disable | status]'
HELP_swap='
  Manage the system swap file.
'
cmd_swap() (
  local swap_enable_file="/home/chronos/.swap_enabled"

  case "$1" in
  "enable")
    if [ -z "$2" ]; then
      # remove file in case root owns it
      rm -f $swap_enable_file
      touch $swap_enable_file
    elif ! echo "$2" | grep -q "^[0-9]*$"; then
      help "$2 is not a valid integer literal"
      return 1
    elif [ "$2" -ne 500 -a \
           "$2" -ne 1000 -a \
           "$2" -ne 2000 -a \
           "$2" -ne 3000 -a \
           "$2" -ne 4000 -a \
           "$2" -ne 4500 -a \
           "$2" -ne 6000 ]; then
      echo "Valid choices: 500, 1000, 2000, 3000, 4000, 4500 or 6000."
      help "invalid size $2."
      return 1
    else
      # remove file in case root owns it
      rm -f $swap_enable_file
      echo "$2" > $swap_enable_file
      echo "You have selected a size of $2 MB for compressed swap."
      echo "Your choice may affect system performance in various ways."
      echo "Presumably you know what you are doing."
    fi
    echo "Swap will be ON at next reboot."
    ;;
  "disable")
    # remove file in case root owns it
    rm -f $swap_enable_file
    echo 0 > $swap_enable_file
    echo "You are turning compressed swap off."
    echo "This may impact system performance in various ways."
    echo "Presumably you know what you are doing."
    echo "Swap will be OFF at next reboot."
    ;;
  "status")
    /sbin/swapon -s
    ;;
  *)
    help "unknown option: $1"
    return 1
    ;;
  esac
)

USAGE_time_info=''
HELP_time_info='
  Returns the current synchronization state for the time service.
'
cmd_time_info() (
  echo "Last time synchronization information:"
  dbus-send --system --type=method_call --print-reply \
      --dest=org.torproject.tlsdate /org/torproject/tlsdate \
      org.torproject.tlsdate.LastSyncInfo 2>/dev/null |
    sed -n \
        -e 's/boolean/network-synchronized:/p' \
        -e 's/string/last-source:/p' \
        -e 's/int64/last-synced-time:/p'
)

USAGE_bt_console='[<agent capability>]'
HELP_bt_console='
  Enters a Bluetooth debugging console. Optional argument specifies the
  capability of a pairing agent the console will provide; see the Bluetooth
  Core specification for valid options.
'
cmd_bt_console() (
  if [ -x /usr/bin/bluetoothctl ]; then
    /usr/bin/bluetoothctl "${1:+--agent=$1}"
  else
    echo "Bluetooth console not available"
  fi
)

USAGE_ff_debug='[<tag_expr>] [--list_valid_tags] [--reset]'
HELP_ff_debug='
  Add and remove flimflam debugging tags.  "ff_debug help" for more details.
'
cmd_ff_debug() (
  if [ "$1" = "help" ]; then
    set -- --help
  fi
  /usr/bin/ff_debug "$@"
)

USAGE_wpa_debug='[<debug_level>] [--list_valid_level] [--reset]'
HELP_wpa_debug='
  Set wpa_supplicant debugging level.  "wpa_debug help" for more details.
'
cmd_wpa_debug() (
  if [ "$1" = "help" ]; then
    set -- --help
  fi
  /usr/bin/wpa_debug "$@"
)

USAGE_set_arpgw='<true | false>'
HELP_set_arpgw='
  Turn on extra network state checking to make sure the default gateway
  is reachable.
'
cmd_set_arpgw() (
  case "$1" in
    "true"|"false")
      /usr/bin/set_arpgw "$1"
      ;;
    "")
      /usr/bin/set_arpgw
      ;;
    *)
      help "unknown option: $1"
      return 1
      ;;
  esac
)

USAGE_set_wake_on_lan='<true | false>'
HELP_set_wake_on_lan='
  Enable or disable Wake on LAN for Ethernet devices.  This command takes
  effect after re-connecting to Ethernet and is not persistent across system
  restarts.
'
cmd_set_wake_on_lan() (
  case "$1" in
    "true"|"false")
      /usr/bin/set_wake_on_lan "$1"
      ;;
    "")
      /usr/bin/set_wake_on_lan
      ;;
    *)
      help "unknown option: $1"
      return 1
      ;;
  esac
)

USAGE_network_logging='<wifi | cellular | ethernet>'
HELP_network_logging='
  A function that enables a predefined set of tags useful for
  debugging the specified device.
'
cmd_network_logging() (
  if [ -n "$1" ]; then
    case "$1" in
      "wifi")
        echo; /usr/bin/ff_debug service+wifi+inet+device+manager
        echo; /usr/bin/wpa_debug msgdump
        echo; /usr/bin/modem set-logging info
        ;;
      "cellular")
        echo; /usr/bin/ff_debug service+cellular+modem+device+manager
        echo; /usr/bin/wpa_debug info
        echo; /usr/bin/modem set-logging debug
        ;;
      "ethernet")
        echo; /usr/bin/ff_debug service+ethernet+device+manager
        echo; /usr/bin/wpa_debug info
        echo; /usr/bin/modem set-logging info
        ;;
      *)
        help "unknown option: $1"
        ;;
    esac
  else
    help "missing command"
  fi
)

USAGE_network_diag="[--date] [--flimflam] [--link] [--show-macs] [--wifi] \
[--wifi-mon] <host>"
HELP_network_diag='
  A function that performs a suite of network diagnostics.  Saves a copy
  of the output to your Downloads directory.
'
cmd_network_diag() (
  /usr/bin/network_diag "$@"
)

debugd() {
  # Default timeout 30 seconds.
  local timeout="--reply-timeout=30000"
  case $1 in
  --reply-timeout=*) timeout=$1; shift;;
  esac
  local method="$1"; shift
  dbus-send ${timeout} --system --print-reply --fixed --dest=org.chromium.debugd \
      /org/chromium/debugd "org.chromium.debugd.$method" "$@"
}

# Run a debugd command for a long time and poll its output.
# This expects Start & Stop methods.
debugd_poll() (
  local methodbase="$1"; shift

  local pid fifo

  # Make sure we clean up the background process and temp files when the
  # user kills us with CTRL+C.
  cleanup() {
    # Don't let the user kill us while cleaning up.
    trap : INT

    if [ -n "${pid}" ]; then
      if ! debugd "${methodbase}Stop" "string:${pid}"; then
        echo "warning: could not stop ${methodbase}"
      fi
      pid=''
    fi

    if [ -n "${fifo}" ]; then
      dir=$(dirname "${fifo}")
      rm -rf "${dir}"
      fifo=''
    fi
  }
  trap cleanup INT

  if ! fifo="$(mk_fifo)"; then
    # The mk_fifo command already showed a warning.
    return 1
  fi
  debugd "${methodbase}Start" "$@" 2>&1 >"${fifo}" &

  read pid < "${fifo}"
  cat "${fifo}"
  cleanup
)

USAGE_ping="[-4] [-6] [-c count] [-i interval] [-n] [-s packetsize] \
[-W waittime] <destination>"
HELP_ping='
  Send ICMP ECHO_REQUEST packets to a network host.  If <destination> is "gw"
  then the next hop gateway for the default route is used.
  Default is to use IPv4 [-4] rather than IPv6 [-6] addresses.
'
cmd_ping() (
  local option="dict:string:variant:"
  local dest

  while [ $# -gt 0 ]; do
    # Do just enough parsing to filter/map options; we
    # depend on ping to handle final validation.
    case "$1" in
    -4) option="${option}v6,boolean:false," ;;
    -6) option="${option}v6,boolean:true," ;;
    -i) shift; option="${option}interval,int32:$1," ;;
    -c) shift; option="${option}count,int32:$1," ;;
    -W) shift; option="${option}waittime,int32:$1," ;;
    -s) shift; option="${option}packetsize,int32:$1," ;;
    -n) option="${option}numeric,boolean:true," ;;
    -b) option="${option}broadcast,boolean:true," ;;
    -*)
      help "unknown option: $1"
      return 1
      ;;
    *)
      if [ "${dest+set}" = "set" ]; then
        help "too many destinations specified"
        return 1
      fi
      dest="$1"
      ;;
    esac

    shift
  done

  if [ "${dest+set}" != "set" ]; then
    help "missing parameter: destination"
    return 1
  fi

  if [ "${dest}" = "gw" ]; then
    # Convenient shorthand for the next-hop gateway attached
    # to the default route; this means if you have a host named
    # "gw" then you'll need to specify a FQDN or IP address.
    dest=$(ip route show | awk '$1 == "default" { print $3 }')
    if [ -z "${dest}" ]; then
      echo "Cannot determine primary gateway; routing table is:"
      cmd_route
      return 1
    fi
  fi

  # Remove trailing comma in the options list if it exists.
  debugd_poll Ping "fd:1" "string:${dest}" "${option%,}"
)

USAGE_chaps_debug='[start|stop|<log_level>]'
HELP_chaps_debug='
  Sets the chapsd logging level.  No arguments will start verbose logging.
'
cmd_chaps_debug() (
  local level=${1:--2}
  if [ "$1" = "stop" ]; then
    level=0
  fi
  if [ "$1" = "start" ]; then
    level=-2
  fi
  rm -f /home/chronos/.chaps_debug_1 /home/chronos/.chaps_debug_2
  if [ "${level}" = "-1" ]; then
    touch /home/chronos/.chaps_debug_1
  elif [ "${level}" = "-2" ]; then
    touch /home/chronos/.chaps_debug_2
  fi
  /usr/bin/chaps_client --set_log_level=${level} 2> /dev/null
  if [ $? -eq 0 ]; then
    echo "Logging level set to ${level}."
  else
    echo "Failed to set logging level."
  fi
)

USAGE_route='[-4] [-6]'
HELP_route='
  Display the routing tables.
  Default is to show IPv4 [-4] rather than IPv6 [-6] routes.
'
cmd_route() (
  local option="dict:string:variant:"

  while [ $# -gt 0 ]; do
    case $1 in
    -4) option="${option}v6,boolean:false," ;;
    -6) option="${option}v6,boolean:true," ;;
    *)
      help "unknown option: $1"
      return 1
    esac
    shift
  done

  debugd GetRoutes "${option%,}"
)

USAGE_tracepath='[-4] [-6] [-n] <destination>[/port]'
HELP_tracepath='
  Trace the path/route to a network host.
  Default is to trace IPv4 [-4] rather than IPv6 [-6] targets.
'
cmd_tracepath() (
  local option="dict:string:variant:"
  local dest

  while [ $# -gt 0 ]; do
    # Do just enough parsing to filter/map options; we
    # depend on tracepath to handle final validation.
    case "$1" in
    -4) option="${option}v6,boolean:false," ;;
    -6) option="${option}v6,boolean:true," ;;
    -n) option="${option}numeric,boolean:true," ;;
    -*)
      help "unknown option: $1"
      return 1
      ;;
    *)
      if [ "${dest+set}" = "set" ]; then
        help "too many destinations specified"
        return 1
      fi
      dest="$1"
      ;;
    esac

    shift
  done

  if [ "${dest+set}" != "set" ]; then
    help "missing parameter: destination"
    return 1
  fi

  # Remove trailing comma in the options list if it exists.
  debugd_poll TracePath "fd:1" "string:${dest}" "${option%,}"
)

USAGE_top=''
HELP_top='
  Run top.
'
cmd_top() (
  # -s is "secure" mode, which disables kill, renice, and change display/sleep
  # interval.
  top -s
)

USAGE_modem='<command> [args...]'
HELP_modem='
  Interact with the 3G modem. Run "modem help" for detailed help.
'
cmd_modem() (
  /usr/bin/modem "$@"
)

USAGE_modem_set_carrier='carrier-name'
HELP_modem_set_carrier='
  Configures the modem for the specified carrier.
'
cmd_modem_set_carrier() (
  /usr/bin/modem set-carrier "$@"
)

USAGE_set_apn='[-c] [-n <network-id>] [-u <username>] [-p <password>] <apn>'
HELP_set_apn='
  Set the APN to use when connecting to the network specified by <network-id>.
  If <network-id> is not specified, use the network-id of the currently
  registered network.

  The -c option clears the APN to be used, so that the default APN will be used
  instead.
'
cmd_set_apn() (
  /usr/bin/set_apn "$@"
)

USAGE_set_cellular_ppp='[-c] [-u <username>] [-p <password>]'
HELP_set_cellular_ppp='
  Set the PPP username and/or password for an existing cellular connection.
  If neither -u nor -p is provided, show the existing PPP username for
  the cellular connection.

  The -c option clears any existing PPP username and PPP password for an
  existing cellular connection.
'
cmd_set_cellular_ppp() (
  /usr/bin/set_cellular_ppp "$@"
)

USAGE_connectivity=''
HELP_connectivity='
  Shows connectivity status.  "connectivity help" for more details
'
cmd_connectivity() (
  /usr/bin/connectivity "$@"
)

USAGE_autest='[-scheduled]'
HELP_autest='
  Trigger an auto-update. To fake a scheduled update check use -scheduled.
'
cmd_autest() (
  local omaha_url="autest"

  if [ "$1" = "-scheduled" ]; then
    # pretend that this is a scheduled check as opposed to an user-initiated
    # check for testing features that get enabled only on scheduled checks.
    omaha_url="autest-scheduled"
  fi

  echo "Calling update_engine_client with omaha_url = $omaha_url"
  /usr/bin/update_engine_client --omaha_url=$omaha_url
)

USAGE_p2p_update='[enable|disable]'
HELP_p2p_update='
  Enables or disables the peer-to-peer (P2P) sharing of updates over the local
  network. This will both attempt to get updates from other peers in the
  network and share the downloaded updates with them. Run this command without
  arguments to see the current state.
'
cmd_p2p_update() (
  case "$1" in
    "enable")
      param="-p2p_update=yes"
      ;;

    "disable")
      param="-p2p_update=no"
      ;;

    "")
      param=""
      ;;

    *)
      help "unknown option: $1"
      return 1
      ;;
  esac
  /usr/bin/update_engine_client $param -show_p2p_update
)

USAGE_rollback=''
HELP_rollback='
  Attempt to rollback to the previous update cached on your system. Only
  available on non-stable channels and non-enterprise enrolled devices. Please
  note that this will powerwash your device.
'
cmd_rollback() (
  if /usr/bin/update_engine_client --rollback; then
    echo "Rollback attempt succeeded -- after a couple minutes you will" \
         "get an update available and you should reboot to complete rollback."
  else
    echo "Rollback attempt failed. Check chrome://system for more information."
  fi
)

USAGE_update_over_cellular='[enable|disable]'
HELP_update_over_cellular='
  Enables or disables the auto updates over cellular networks. Run without
  arguments to see the current state.
'
cmd_update_over_cellular() (
  case "$1" in
    "enable")
      param="-update_over_cellular=yes"
      echo "When available, auto-updates download in the background any time " \
           "the computer is powered on.  Note: this may incur additional " \
           "cellular charges, including roaming and/or data charges, as per " \
           "your carrier arrangement."
      ;;

    "disable")
      param="-update_over_cellular=no"
      ;;

    "")
      param=""
      ;;

    *)
      help "unknown option: $1"
      return 1
      ;;
  esac
  /usr/bin/update_engine_client $param -show_update_over_cellular
)

USAGE_upload_crashes=''
HELP_upload_crashes='
  Uploads available crash reports to the crash server.
'
cmd_upload_crashes() (
  debugd UploadCrashes
  echo "Check chrome://crashes for status updates"
)

USAGE_upload_devcoredumps='[enable|disable]'
HELP_upload_devcoredumps='
  Enable or disable the upload of devcoredump reports.
'
cmd_upload_devcoredumps() (
  case "$1" in
    "enable")
      debugd EnableDevCoredumpUpload
      ;;

    "disable")
      debugd DisableDevCoredumpUpload
      ;;

    *)
      help "unknown option: $1"
      return 1
      ;;
  esac
)

USAGE_try_touch_experiment='<experiment hex>'
HELP_try_touch_experiment='
  Manage touchpad experiments.
'
cmd_try_touch_experiment() (
  /usr/sbin/try_touch_experiment "$@"
)

USAGE_inputcontrol=''
HELP_inputcontrol='
  Manually adjust advanced touchpad and mouse settings. Run
  "inputcontrol help" for full list of options.
'
cmd_inputcontrol() (
  if [ "$1" = "help" ]; then
    set -- -h
  fi
  /opt/google/input/inputcontrol "$@"
)

USAGE_rlz='< status | enable | disable >'
HELP_rlz="
  Enable or disable RLZ.  See this site for details:
  http://dev.chromium.org/developers/design-documents/extensions/\
proposed-changes/apis-under-development/rlz-api
"
cmd_rlz() (
  local flag_file="$HOME/.rlz_disabled"
  local enabled=1
  local changed=0
  if [ -r "${flag_file}" ]; then
    enabled=0
  fi
  case "$1" in
    "status")
      if [ $enabled -eq 1 ]; then
        echo "Currently enabled"
      else
        echo "Currently disabled"
      fi
      return
      ;;

    "enable")
      if [ $enabled -eq 0 ]; then
        changed=1
      fi
      rm -f "${flag_file}"
      ;;

    "disable")
      if [ $enabled -eq 1 ]; then
        changed=1
      fi
      touch "${flag_file}"
      ;;

    *)
      help "unknown option: $1"
      return 1
      ;;
  esac
  if [ $changed -eq 1 ]; then
    echo "You must reboot for this to take effect."
  else
    echo "No change."
  fi
)

USAGE_syslog='<message>'
HELP_syslog='
  Logs a message to syslog (the system log daemon).
'
cmd_syslog() (
  logger -t crosh -- "$*"
)

mk_fifo() {
  local dir fifo

  # We want C-c to terminate the running test so that the UI stays the same.
  # Therefore, create a fifo to direct the output of the test to, and have a
  # subshell read from the fifo and emit to stdout. When the subshell ends (at a
  # C-c), we stop the test and clean up the fifo.
  # no way to mktemp a fifo, so make a dir to hold it instead
  dir=$(mktemp -d "/tmp/crosh-test-XXXXXXXXXX")
  if [ $? -ne 0 ]; then
    echo "Can't create temporary directory"
    return 1
  fi
  fifo="${dir}/fifo"
  if ! mkfifo "${fifo}"; then
    echo "Can't create fifo at ${fifo}"
    return 1
  fi

  echo "${fifo}"
}

USAGE_storage_test_1=''
HELP_storage_test_1='
  Performs a short offline SMART test.
'
cmd_storage_test_1() (
  option="$1"

  debugd Smartctl "string:abort_test" >/dev/null

  test=$(debugd Smartctl "string:short_test")
  if [ "$option" != "-v" ]; then
    echo "$test" | sed -n '1p;2p'
    echo ""
    echo "$test" | grep "Please wait"
  else
    echo "$test"
  fi

  echo ""

  while debugd Smartctl "string:capabilities" |
        grep -q "of test remaining"; do
    true
  done

  result=$(debugd Smartctl "string:selftest")
  if [ "$option" != "-v" ]; then
    echo "$result" | grep -e "Num" -e "# 1"
  else
    echo "$result"
  fi

  debugd Smartctl "string:abort_test" >/dev/null
)

USAGE_storage_test_2=''
HELP_storage_test_2='
  Performs an extensive readability test.
'
cmd_storage_test_2() (
  debugd_poll Badblocks "fd:1"
)

# Set the vars to pass the unittests ...
USAGE_storage_status=''
HELP_storage_status=''
# ... then unset them to hide the command from "help" output.
unset USAGE_storage_status HELP_storage_status
cmd_storage_status() (
  echo "Removed. See storage_info section in chrome://system"
)

USAGE_memory_test=''
HELP_memory_test='
  Performs extensive memory testing on the available free memory.
'
cmd_memory_test() (
  # Getting total free memory in KB.
  mem=$(cat /proc/meminfo | grep MemFree | tr -s " " | cut -d" " -f 2)

  # Converting to MB.
  mem=$(($mem / 1024))

  # Giving OS 200MB free memory before hogging the rest of it.
  mem=$(($mem - 200))

  debugd_poll Memtester "fd:1" "uint32:${mem}"
)

USAGE_battery_firmware='<info|check|update>'
HELP_battery_firmware='
  info   : Query battery info.
  check  : Check whether the AC adapter is connected.
           Also check whether the battery firmware is the latest.
  update : Trigger battery firmware update.
'
cmd_battery_firmware() (
  option="$1"
  case "${option}" in
    info|check|update)
      # Increased the reply-timeout to 10 min for Battery Firmware update process.
      # Battery Firmware Update process time includes the following:
      #   1  setup delay time before entery battery firmware update mode.
      #   2  battery flash erase time.
      #   3  data transfer time from AP to EC, and then to Batttery.
      #   4  battery flash program/write time.
      #   5  re-try time on errors.
      # Note: take ~2 min on a success battery firmware update case.
      debugd --reply-timeout=$(( 10 * 60 * 1000 )) BatteryFirmware "string:${option}"
      echo ""
      ;;
    *)
      help "Unknown option: ${option}"
      ;;
  esac
)

USAGE_battery_test='[<test length>]'
HELP_battery_test='
  Tests battery discharge rate for given number of seconds. Without an argument,
  defaults to 300 seconds.
'
cmd_battery_test() (
  local test_length="$1"
  if [ -z "$test_length" ]; then
    echo "No test length specified. Defaulting to 300 seconds."
    test_length=300
  fi

  if ! check_digits "${test_length}"; then
    echo "Invalid test length."
    return 1
  fi

  if [ "$(get_power_status_field 'battery_present')" != '1' ]; then
    echo "No battery found."
    return 1
  fi

  if [ "$(get_power_status_field 'battery_discharging')" = '1' ]; then
    local bat_status='discharging'
    local bat_discharging=1
  else
    local bat_status='charging or full'
    local bat_discharging=0
  fi

  local bat_pct="$(get_power_status_field 'battery_percent')"
  local bat_full="$(get_power_status_field 'battery_charge_full')"
  local bat_full_design="$(get_power_status_field 'battery_charge_full_design')"
  local bat_health="$(echo "${bat_full}" "${bat_full_design}" | \
      awk '{ printf "%.2f", 100.0 * $1 / $2 }')"

  echo "Battery is ${bat_status} (${bat_pct}% left)"
  echo "Battery health: ${bat_health}%"

  if [ "${bat_discharging}" != '1' ]; then
    echo "Please make sure the power supply is unplugged and retry the test."
    return 1
  fi

  echo "Please wait..."
  sleep "${test_length}"

  local bat_after="$(get_power_status_field 'battery_percent')"
  local bat_diff="$(echo "${bat_pct}" "${bat_after}" | \
      awk '{ printf "%.2f", $1 - $2 }')"
  echo "Battery discharged ${bat_diff}% in ${test_length} second(s)."
)

USAGE_dump_emk=''
HELP_dump_emk='
  Show the EMK (Enterprise Machine Key).
'
cmd_dump_emk() (
  /usr/sbin/cryptohome --action=tpm_attestation_key_status \
                       --name=attest-ent-machine
)

USAGE_tpm_status=''
HELP_tpm_status='
  Prints TPM (Trusted Platform Module) status information.
'
cmd_tpm_status() (
  /usr/sbin/cryptohome --action=tpm_more_status
)

substr() {
  local str="$1"
  local start="$2"
  local end="$3"

  if [ "$IS_BASH" = "1" ]; then
    # NB: use printf to avoid echo interpreting -n
    if [ -z "$end" ]; then
      printf '%s\n' ${str:$start}
    else
      printf '%s\n' ${str:$start:$end}
    fi
    return
  fi

  start=$(expr "$start" + 1)

  if [ ! -z "$end" ]; then
    end=$(expr "$end" - 1)
  fi

  echo "$str" | cut -c${start}-${end}
}

# Return true if the first arg is a valid command.
check_command() {
  local command="$1"

  type "cmd_${command}" 2>/dev/null | head -1 | grep -q "function"
}

dispatch() {
  local line="$1"
  local command=""
  local p params=""

  local space_pos=$(expr index "$line" ' ')

  if [ $space_pos = 0 ]; then
    command=$line
  else
    command=$(substr "$line" "0" "$space_pos")
    command=${command% *}
    params=$(substr "$line" "$space_pos")
  fi

  if ! check_command "${command}"; then
    help "unknown command: ${command}"
  else
    # See if --help was requested; if so, handle it directly so each command
    # doesn't have to deal with it directly.
    local p
    for p in ${params}; do
      if [ "${p}" = "-h" -o "${p}" = "--help" ]; then
        cmd_help "${command}"
        return 0
      fi
    done

    # Set CURRENT_COMMAND for the `help` helper.
    CURRENT_COMMAND="${command}" "cmd_${command}" ${params}
  fi
}

repl() {
  echo "${INTRO_TEXT}"
  if [ "$IS_BASH" != "1" ]; then
    echo "Sorry, line editing and command history disabled due to" \
      "shell limitations."
  fi

  # This will be set by the 'exit' command to tell us to quit.
  local exit

  # Create a colorized prompt to make it easier to see commands start/finish.
  local prompt
  prompt="$(printf '%bcrosh>%b ' '\033[1;33m' '\033[0m')"

  while [ -z "${exit}" ]; do
    if shell_read "${prompt}" LINE_; then
      if [ ! -z "$LINE_" ]; then
        shell_history -s "$LINE_"
        dispatch "$LINE_"
      fi
    else
      echo
      return 1
    fi
  done
}

main() {
  load_extra_crosh "$@"

  INPUTRC="/usr/share/misc/inputrc.crosh"
  if [ ! -e "${INPUTRC}" ]; then
    # We aren't installed; use local copy for testing.
    INPUTRC="$(dirname "$0")/inputrc.crosh"
  fi

  shell_history_init

  repl

  # Save the history after the clean exit.
  shell_history -w "${HISTFILE}"
}
main "$@"
